/*
 * Copyright (C) 2010 Fores Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jp.fores.midori.server;

import java.io.IOException;
import java.util.Enumeration;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import jp.fores.midori.core.util.StringUtil;
import jp.fores.midori.server.config.MidoriFilterConfig;

import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * フィルタークラス。<br>
 */
public class MidoriFilter implements Filter {
	//==========================================================
	//定数

	/**
	 * ログ出力用
	 */
	private static final Log log = LogFactory.getLog(MidoriFilter.class);


	//==========================================================
	//フィールド

	/**
	 * フィルターの設定内容を保持するクラス
	 */
	protected MidoriFilterConfig config = null;


	//==========================================================
	//メソッド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//抽象メソッドの実装

	/**
	 * 初期化処理を行います。<br>
	 *
	 * @param filterConfig フィールターコンフィグ
	 * @throws ServletException サーブレット例外
	 */
	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		//==========================================================
		//フィールターコンフィグの内容をフィルターの設定内容を保持するクラスに詰め替える

		//フィルターの設定内容を保持するクラスのインスタンスを生成
		MidoriFilterConfig config = new MidoriFilterConfig();

		//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
		//文字エンコーディング
		//(深い意味はないが、変数の誤使用を避けるためにスコープを分ける)
		{
			//フィールターコンフィグの値を取得
			String configValue = filterConfig.getInitParameter("encoding");

			//取得した値がnullまたは空文字列でない場合
			if ((configValue != null) && !"".equals(configValue)) {
				//フィルターの設定内容を保持するクラスに、取得した値をそのまま設定
				config.encoding = configValue;
			}
		}


		//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
		//キャッシュの破棄を行うかどうかのフラグ
		//(深い意味はないが、変数の誤使用を避けるためにスコープを分ける)
		{
			//フィールターコンフィグの値を取得
			String configValue = filterConfig.getInitParameter("expireCache");

			//取得した値がnullまたは空文字列でない場合
			if ((configValue != null) && !"".equals(configValue)) {
				//フィルターの設定内容を保持するクラスに、取得した値をbooleanに変換して設定
				config.expireCache = Boolean.parseBoolean(configValue);
			}
		}


		//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
		//リクエストをgzip展開するかどうかのフラグ
		//(深い意味はないが、変数の誤使用を避けるためにスコープを分ける)
		{
			//フィールターコンフィグの値を取得
			String configValue = filterConfig.getInitParameter("requestGZip");

			//取得した値がnullまたは空文字列でない場合
			if ((configValue != null) && !"".equals(configValue)) {
				//フィルターの設定内容を保持するクラスに、取得した値をbooleanに変換して設定
				config.requestGZip = Boolean.parseBoolean(configValue);
			}
		}


		//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
		//レスポンスをgzip圧縮するかどうかのフラグ
		//(深い意味はないが、変数の誤使用を避けるためにスコープを分ける)
		{
			//フィールターコンフィグの値を取得
			String configValue = filterConfig.getInitParameter("responseGZip");

			//取得した値がnullまたは空文字列でない場合
			if ((configValue != null) && !"".equals(configValue)) {
				//フィルターの設定内容を保持するクラスに、取得した値をbooleanに変換して設定
				config.responseGZip = Boolean.parseBoolean(configValue);
			}
		}
		//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/=


		//デバッグログが有効な場合
		if (log.isDebugEnabled()) {
			//デバッグログ出力
			log.debug("MidoriFilterConfigの設定内容:" + config);
		}

		//作成したフィルターの設定内容を保持するクラスのインスタンスを、フィールドに設定
		this.config = config;
	}

	/**
	 * 終了処理を行います。
	 */
	@Override
	public void destroy() {
		//特に何も行わない
	}

	/**
	 * フィルター処理を行います。<br>
	 *
	 * @param request 現在のリクエスト情報
	 * @param response 現在のレスポンス情報
	 * @param chain フィルターチェーン
	 * @throws ServletException サーブレット例外
	 * @throws IOException 入出力例外
	 */
	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		//リクエストとレスポンスがHTTP用のクラスの場合
		if ((request instanceof HttpServletRequest)
				&& (response instanceof HttpServletResponse)) {
			//実際に処理を行うメソッドを呼び出す
			doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
		}
		//それ以外の場合
		else {
			//チェーン内の次のフィルターを呼び出す
			chain.doFilter(request, response);
		}
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//内部処理用

	/**
	 * フィルター処理を行います。<br>
	 *
	 * @param request 現在のリクエスト情報
	 * @param response 現在のレスポンス情報
	 * @param chain フィルターチェーン
	 * @throws ServletException サーブレット例外
	 * @throws IOException 入出力例外
	 */
	protected void doFilter(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		//デバッグログが有効な場合
		if (log.isDebugEnabled()) {
			//デバッグログ出力
			log.debug("リクエストの内容:"
					+ RequestAnalyzer.toStringAllRequestInfo(request));
		}


		//==========================================================
		//文字エンコーディング

		//設定内容の文字エンコーディングがnullまたは空文字列でない場合
		if ((this.config.encoding != null) && !"".equals(this.config.encoding)) {
			//リクエストとレスポンスに文字エンコーディングを設定
			request.setCharacterEncoding(this.config.encoding);
			response.setCharacterEncoding(this.config.encoding);
		}


		//==========================================================
		//キャッシュの破棄

		//設定内容のキャッシュの破棄を行うかどうかのフラグがたっている場合
		if (this.config.expireCache) {
			//レスポンスのヘッダにキャッシュの破棄用の情報を設定する
			response.setHeader("Cache-Control", "no-cache");
			response.setHeader("Pragma", "no-cache");
			response.setHeader("Expires", "Thu, 01 Jan 1970 00:00:00 GMT");
		}


		//==========================================================
		//リクエスト・レスポンスのgzip対応

		//リクエストの拡張パス情報を取得
		String path = request.getPathInfo();

		//拡張パス情報が空文字列でなく、かつ「.」が含まれている場合のみ処理を行う
		//(「.」なしのアクセスの場合は、リスティング(サービス一覧やメソッド一覧の情報)を表示したいので、
		// 圧縮されると都合が悪いため)
		if (!StringUtil.isBlank(path) && (path.indexOf(".") != -1)) {
			//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
			//リクエストのgzip対応

			//設定内容のリクエストをgzip展開するかどうかのフラグがたっている場合
			if (this.config.requestGZip) {
				//リクエストの「Content-Encoding」ヘッダの内容をEnumerationで取得
				//(単一の文字列ではなく、Enumerationで取得するのは、同名のヘッダに複数の値が設定されている可能性もあるため)
				Enumeration<String> enume = request.getHeaders("Content-Encoding");

				//Enumerationが取得できなかった場合、または空の場合
				if ((enume == null) || !enume.hasMoreElements()) {
					//リクエストのGAE向けの「Content-Encoding」ヘッダの内容をEnumerationで取得
					enume = request.getHeaders("x-GAE-Content-Encoding");
				}

				//Enumerationが取得できた場合
				//(コンテナの制限によっては取得できないこともあるので、念のためにチェック)
				if (enume != null) {
					//Enumerationに対してループをまわす
					while (enume.hasMoreElements()) {
						//Enumerationの現在の値を取得
						String header = enume.nextElement();

						//取得した値に「gzip」という文字列が含まれている場合
						if (header.indexOf("gzip") != -1) {
							//デバッグログが有効な場合
							if (log.isDebugEnabled()) {
								//デバッグログ出力
								log.debug("リクエストのgzip対応を行います。");
							}

							//リクエストをgzip対応のクラスでラッピングする
							request = new GZipHttpServletRequest(request);

							//ループを抜ける
							break;
						}
					}
				}
			}


			//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
			//レスポンスのgzip対応

			//設定内容のレスポンスをgzip圧縮するかどうかのフラグがたっている場合
			if (this.config.responseGZip) {
				//レスポンスの「Content-Encoding」用のヘッダ名
				String responseContentEncodingHeaderName = "Content-Encoding";

				//リクエストの「Accept-Encoding」ヘッダの内容をEnumerationで取得
				//(単一の文字列ではなく、Enumerationで取得するのは、同名のヘッダに複数の値が設定されている可能性もあるため)
				Enumeration<String> enume = request.getHeaders("Accept-Encoding");

				//Enumerationが取得できなかった場合、または空の場合
				if ((enume == null) || !enume.hasMoreElements()) {
					//リクエストのGAE向けの「Accept-Encoding」ヘッダの内容をEnumerationで取得
					enume = request.getHeaders("x-GAE-Accept-Encoding");

					//レスポンスの「Content-Encoding」用のヘッダ名にGAE用の値を設定する
					responseContentEncodingHeaderName = "x-GAE-Content-Encoding";
				}

				//Enumerationが取得できた場合
				//(コンテナの制限によっては取得できないこともあるので、念のためにチェック)
				if (enume != null) {
					//Enumerationに対してループをまわす
					while (enume.hasMoreElements()) {
						//Enumerationの現在の値を取得
						String header = enume.nextElement();

						//取得した値に「gzip」という文字列が含まれている場合
						if (header.indexOf("gzip") != -1) {
							//デバッグログが有効な場合
							if (log.isDebugEnabled()) {
								//デバッグログ出力
								log.debug("レスポンスのgzip対応を行います。");
							}

							//レスポンスをgzip対応のクラスでラッピングする
							response = new GZipHttpServletResponse(response);

							//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
							//リクエストの内容に応じてレスポンスヘッダに設定する値を切り替える

							//取得した値に「x-gzip」という文字列が含まれている場合
							if (header.indexOf("x-gzip") != -1) {
								//レスポンスの「Content-Encoding」用のヘッダに、「x-gzip」という値を設定
								response.setHeader(responseContentEncodingHeaderName, "x-gzip");
							}
							//それ以外の場合
							else {
								//レスポンスの「Content-Encoding」用のヘッダに、「gzip」という値を設定
								response.setHeader(responseContentEncodingHeaderName, "gzip");
							}
							//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/


							//ループを抜ける
							break;
						}
					}
				}
			}
		}


		try {
			//==========================================================
			//チェーン内の次のフィルターを呼び出す
			chain.doFilter(request, response);
		}
		//終了処理
		finally {
			//リクエストがgzip対応のクラスの場合
			if (request instanceof GZipHttpServletRequest) {
				//リクエストの入力ストリームを確実に閉じる
				//(普通のリクエストクラスだと、getInputStream()を複数回呼ぶとエラーになってしまうので、gzip対応のクラスの場合のみ処理を行う)
				IOUtils.closeQuietly(request.getInputStream());
			}

			//レスポンスの入力ストリームを確実に閉じる
			//(普通のレスポンスクラスだと、getOutputStream()を複数回呼ぶとエラーになってしまうので、gzip対応のクラスの場合のみ処理を行う)
			if (response instanceof GZipHttpServletResponse) {
				IOUtils.closeQuietly(response.getOutputStream());
			}
		}
	}

}
